import BaseHTTPServer, select, SocketServer, urlparse, const, config, socket, ssl, os.path, logger
from prob import BiasedCoin
from threading import Thread

__doc__ = """

This module implements GET, HEAD, POST, PUT and DELETE methods
on BaseHTTPServer, and behaves as an HTTP proxy.  The CONNECT
method is also implemented experimentally, but has not been
tested yet.

"""
__version__ = "0.0.0.1"

class ProxyServer(Thread):
    def __init__(self, port, blender, crowd, path_manager, thread_name="ProxyServer"):
        Thread.__init__(self, name=thread_name)
        self.setDaemon(1)
        self.port = port
        ProxyHandler.crowd = crowd               
        ProxyHandler.blender = blender
        ProxyHandler.path_manager = path_manager
        
    def run(self):
        ProxyHandler.logger = logger.setup_logging(self.name, config.LOG_PROXY)
        host_name = socket.gethostname()
        host_ip = socket.gethostbyname(host_name)        
        server_class = ThreadingHTTPServer      # for single thread use: BaseHTTPServer.HTTPServer instead of ThreadingHTTPServer    
        server_address = (host_ip , self.port)  # listen only for local (browser) connections
        httpd = server_class(server_address, ProxyHandler)          
        print self.name + " listening on %s : %s "  % server_address
        httpd.serve_forever()
            
       
class SecureProxyServer(ProxyServer):
    def __init__(self, port, blender, crowd, path_manager, certfile):
        ProxyServer.__init__(self, port, blender, crowd, path_manager, thread_name="SecureProxyServer")
        self.certfile = certfile
        
    def run(self):        
        SecureProxyHandler.logger = logger.setup_logging(self.name, config.LOG_SSL_PROXY)
        server_class = SecureThreadingHTTPServer #not threaded use: BaseHTTPServer.HTTPServer    
        server_address = ('' , self.port)
        httpd = server_class(server_address, SecureProxyHandler, self.certfile)          
        print self.name + " listening on %s : %s "  % server_address
        httpd.serve_forever()
        
    
class ThreadingHTTPServer(SocketServer.ThreadingMixIn,
                           BaseHTTPServer.HTTPServer):
                            pass
                                                
                        
class SecureThreadingHTTPServer(ThreadingHTTPServer):
    def __init__(self, server_address, HandlerClass, cert_pem):
        ThreadingHTTPServer.__init__(self, server_address, HandlerClass)
        if config.SSL_CONNECTION:
            self.socket = ssl.SSLSocket(self.socket, server_side=True,
                                certfile=cert_pem,
                                ssl_version=ssl.PROTOCOL_TLSv1)
            
        
class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
    __base = BaseHTTPServer.BaseHTTPRequestHandler
    __base_handle = __base.handle
             
    server_version = "ProxyHandler/" + __version__        
    rbufsize = 0                        # self.rfile Be unbuffered
        
    def handle(self):
        (ip, port) =  self.client_address
        self.logger.debug("Incoming connection from " + str(self.client_address))
        if  self.crowd.check_jondo(ip):     # check if incoming jondo is allowed to connect
            self.__base_handle()            
        else:
            self.raw_requestline = self.rfile.readline()    
            if self.parse_request(): 
                self.send_error(code=403, message="Forbidden connection or empty crowd" )            
     
    def parse_request(self):
        req = self.__base.parse_request(self)
        if req:
            self.sanitize_request()
            return req        
        
    def sanitize_request(self):
        """remove from the given request the headers in const.HEADERS_NOT_ALLOWED.
        If one of the matching header has got a dictionary value different 
        to 'None', this will change the actual header's value to that. (For instance: 
        header['connection'] = 'close') 
        """ 
        for h, v in self.headers.items():
            try:
                val = config.HEADERS_NOT_ALLOWED[h]
                if val:
                    self.headers[h] = val
                else:
                    del self.headers[h]
            except KeyError as inst: 
                pass 
            
    def _route(self, netloc):
        """
        route request to other random jondo (possibly itself too)

        arguments:
        netloc -- the destination we're connecting to
        
        return:
        (sock, forward): ssl socket connected to dest, forward = 1 -> request forwarded; forward = 0 connected to destination
        """
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        src_id = self.headers.getheader(const.SOURCE_PATH_ID_HEADER)
        self.logger.debug("source-path-id: " + str(src_id))   
        path = self.path_manager.translate_path(src_id)
        if not path :   #no first path has been built yet
            return (self._build_and_forward(sock, src_id), True) 
        else:
            return (self._follow_path(sock, path), True)        
        return (None, None) #something went wrong (should never be executed)....
    
    def _build_and_forward(self, sock, src_id):
        path, jondo = self.path_manager.build_path(self.crowd, sock, src_id, self.headers)
        if (path, jondo) != (None, None):
            self.logger.debug("Building new path '" + path.id + "' and forwarding request to: " + str(path.next_jondo.host_port))
            if config.SSL_CONNECTION:            
                return ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLSv1) # wrap connected socket into SSL socket
            else:
                return sock
        else:
            return None 

    def _follow_path(self, sock, path):
        self.logger.debug ("Using existing path: " + path.id)
        if self.path_manager.follow_through(path, sock, self.headers):
            if config.SSL_CONNECTION:
                return ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLSv1)   # wrap connected socket into SSL socket
            else:
                return sock
        else:
            return None   
        
    def do_GET(self):
        (scm, netloc, path, params, query, fragment) = urlparse.urlparse(self.path, 'http')        
        if scm not in const.PROTOCOL_SUPPORTED or fragment or not netloc: 
            self.send_error(400, "bad url " + self.path)
            return
        self.logger.debug("Request: " + self.raw_requestline.strip())
        with self.crowd.lock:
            self.logger.debug("Acquiring lock on crowd")
            (sock, forward) = self._route(netloc)
            self.logger.debug("Forward request: %s" % forward)
            self.logger.debug("Lock on crowd removed")
        if sock:        # check if it's all ok...
            try:
                #self.log_request() # uncomment to enable console request logging
                command = self.command
                content = ""
                request_version = self.request_version
                if not forward:
                    scm = ''
                    netloc = ''
                    fragment = ''
                content = urlparse.urlunparse((scm, netloc, path, params, query, fragment))
                request = "%s %s %s\r\n" % (command, content, request_version)
                sock.send(request)
                self.headers['Connection'] = "close"        
                self.logger.debug("Transferring data with: " + str(sock.getpeername()))
                for key_val in self.headers.items():
                    sock.send("%s: %s\r\n" % key_val)
                sock.send("\r\n")       # close the request 
                self._read_write(sock)
            except Exception as inst: 
                self.logger.debug("Exception type '" + str(type(inst)) + "': " + str(inst.args))
            finally:
                if sock:
                    try:
                        self.logger.debug("Bye! Closing connection with: " + str(sock.getpeername()))
                        sock.close()
                    except Exception: pass
                if self.connection:
                    try:
                        self.logger.debug("Bye! Closing connection with: " + str(self.connection.getpeername()))
                        self.connection.close()
                    except Exception: pass      
        else:
            self.logger.debug("Unable to connect due to network error (Check if there's any available jondo in the crowd)")
            self.send_error(404, "Unable to connect due to network error (Check if there's any available jondo in the crowd)")
            
    
    def do_CONNECT(self):
        self.logger.debug("CONNECT REJECTED FOR REQUEST: %s " % self.raw_requestline)
        """
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            if self._connect_to(self.path, sock):
                self.log_request(200)
                self.wfile.write(self.protocol_version +
                                 " 200 Connection established\r\n")
                self.wfile.write("Proxy-agent: %s\r\n" % self.version_string())
                self.wfile.write("\r\n")
                self._read_write(sock, 300)
        finally:
            self.logger.debug("Bye: connection closed")
            sock.close()
            self.connection.close()
        """
    def do_GET_OLD(self):
        (scm, netloc, path, params, query, fragment) = urlparse.urlparse(
            self.path, 'http')        
        if scm != 'http' or fragment or not netloc: 
            self.send_error(400, "bad url %s" % self.path)
            return
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            if self._connect_to(netloc, sock):
                if not self.__forward_req:
                    scm = ''
                    netloc = ''
                    fragment = ''
                self.log_request()
                sock.send("%s %s %s\r\n" % (
                    self.command,
                    urlparse.urlunparse((scm, netloc, path, params, query, fragment)),
                    self.request_version))
                                
                self.headers['Connection'] = 'close'        
                self.headers['User-Agent'] = 'Crowds ver. 0.0.0.1'        
                
                for key_val in self.headers.items():
                    sock.send("%s: %s\r\n" % key_val)
                sock.send("\r\n")
                self._read_write(sock)
            else:
                self.send_error(404, "Unable to connect")
        finally:
            print "\t" "bye"
            sock.close()
            self.connection.close()
    def do_GET_OLD2(self):
        (scm, netloc, path, params, query, fragment) = urlparse.urlparse(
            self.path, 'http')
        print netloc
        print self.headers
        if scm != 'http' or fragment or not netloc: 
            self.send_error(400, "bad url %s" % self.path)
            return
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            if self._connect_to(netloc, sock):
                self.log_request()
                sock.send("%s %s %s\r\n" % (
                    self.command,
                    urlparse.urlunparse(('', '', path, params, query, '')),
                    self.request_version))
                self.headers['Connection'] = 'close'
                del self.headers['Proxy-Connection']
                for key_val in self.headers.items():
                    sock.send("%s: %s\r\n" % key_val)
                sock.send("\r\n")
                self._read_write(sock)
        finally:
            print "\t" "bye"
            sock.close()
            self.connection.close()


    def _read_write(self, sock):
        """The following function simply outputs into the socket everything
        that it gets from the client connection. This is the fastest way of
        performing proxy-like communication although requests and responses
        cannot be easily observed."""

        iw = [self.connection, sock]        
        from_peer = str(self.connection.getpeername())
        to_peer = str(sock.getpeername())
        ow = []
        count = 0   
        while 1:
            count += 1
            (ins, _, exs) = select.select(iw, ow, iw, 3)
            if exs: 
                break
            if ins:
                for i in ins:
                    if i is sock:
                        out = self.connection
                    else:
                        out = sock
                    try: data = i.recv(const.BUFFER_SIZE) # if not catched, the statement may fail when SSL! bug?
                    except: 
                        data = ''
                    if data:
                        try: 
                            out.send(data)
                        except Exception as exc:
                            self.logger.debug(str(type(exc)) + " " + str(exc.args))    # log the exception
                        finally:
                            count = 0
            else:
                self.logger.debug("\t" "Idle " + str(count) + " connection bridge: " + from_peer + "<->" + to_peer) 
                if count == const.MAX_IDLING:
                    break        
        
    do_HEAD = do_GET
    do_POST = do_GET
    do_PUT = do_GET
    do_DELETE = do_GET
    

class SecureProxyHandler(ProxyHandler):
    __base = BaseHTTPServer.BaseHTTPRequestHandler
    __base_handle = __base.handle
             
    server_version = "SecureProxyHandler/" + __version__        
    rbufsize = 0                        # self.rfile Be unbuffered
        
    def parse_request(self):
        return self.__base.parse_request(self)
        
    def _route(self, netloc):
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        (ip, port) =  self.client_address
        src_id = self.headers.getheader(const.SOURCE_PATH_ID_HEADER)
        path = self.path_manager.translate_path(src_id)
        self.logger.debug ("The incoming request has source id: '" + src_id + "'")
        if not path:
            coin = BiasedCoin()
            res = coin.flip()
            self.logger.debug ("Coin flip result: " + res)
            if res == 'Tail':
                return (self._build_and_forward(sock, src_id), True) 
            else: # coin gives 'Head'
                return (self._build_and_submit(sock, src_id, netloc), False)
        else:
            return (self._follow_path(sock, path, netloc), not path.is_last)
        return (None, None)   # should never be executed...

    def _build_and_submit(self, sock, src_id, netloc):
        path, jondo = self.path_manager.build_path(self.crowd, sock, src_id, self.headers, True)
        if path:
            self.logger.debug("Building new path '" + path.id + "' to destination: " + netloc)
            return self.path_manager.connect_to_dest(netloc, sock, self.headers)  # socket connected to end server
        else:
            return None  # an error occurs...
        
        
    def _follow_path(self, sock, path, netloc):
        if path.is_last:
            self.logger.debug ("Using existing path: '" + path.id + "' to: " + netloc)
            return self.path_manager.connect_to_dest(netloc, sock, self.headers)
        else:
            next_jondo = self.path_manager.follow_through(path, sock, self.headers)
            if next_jondo:
                self.logger.debug ("Using existing path: '" + path.id + "' to: " + str(next_jondo.host_port))
                if config.SSL_CONNECTION:
                    return ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLSv1)   # wrap connected socket into SSL socket
                else:
                    return sock
            else:
                self.logger.debug ("Error using existing path: '" + path.id + "'")
                return None   

